Understanding React Native Rendering and Performance
Course: React Native Training
Topic: Understanding React Native Rendering and Performance
Course Link: React Native Training at JustAcademy
Demo Link: Register for a Course Demo
Learning Resource: JustAcademy Official Website
1. Introduction to React Native Rendering and Performance
React Native is a framework for building native mobile applications using JavaScript or TypeScript and React. It allows developers to create Android and iOS applications with reusable components, state management, event handling, and native platform capabilities.
Rendering is the process through which React determines what the user interface should look like and React Native updates the native views to reflect that result. Performance describes how efficiently the application performs tasks such as rendering components, handling user interactions, scrolling lists, processing data, and displaying animations.
Understanding rendering and performance is essential because an application may work correctly but still feel slow if it repeatedly renders unnecessary components, performs expensive calculations, loads too much data, or blocks the JavaScript thread.
1.1 Learning Objectives
- Understand the React Native rendering process.
- Differentiate React rendering from native view updates.
- Understand how props, state, context, and parent updates trigger rendering.
- Identify unnecessary re-renders and expensive calculations.
- Use React.memo, useMemo, and useCallback appropriately.
- Improve FlatList and ScrollView performance.
- Understand JavaScript-thread and UI-thread responsibilities.
- Optimize images, animations, navigation, and application startup.
- Measure performance using profiling and debugging tools.
- Apply practical techniques to build smooth, responsive mobile applications.
2. What Is Rendering in React Native?
Rendering is the process of calculating the user interface that should appear based on the current component structure, props, state, and context. React evaluates components and determines whether the resulting interface differs from the previous result.
React Native then coordinates the necessary updates to the native interface. Depending on the React Native architecture and the type of change, this involves communication between JavaScript and native components and work performed by native rendering systems.
2.1 Simple Rendering Example
import React, { useState } from 'react';
import { View, Text, Button } from 'react-native';
export default function CounterScreen() {
const [count, setCount] = useState(0);
return (
<View>
<Text>Count: {count}</Text>
<Button
title="Increase"
onPress={() => setCount(previous => previous + 1)}
/>
</View>
);
}
When the user presses the button, the state changes. React renders the component again using the new value of count, calculates the updated interface, and applies the necessary changes. The entire native screen does not necessarily need to be recreated.
2.2 Rendering Does Not Always Mean a Visible Change
A component can execute again even when the final visible output remains the same. For example, a parent component may render because its own state changed, causing its child component function to execute again.
React compares the new result with the previous result and coordinates updates when necessary. Therefore, component execution and actual native view changes are related but different events.
3. The React Native Rendering Lifecycle
At a high level, React rendering can be understood through three stages: triggering an update, rendering the component tree, and committing the resulting changes. React Native also performs platform-specific work to display the interface.
3.1 Stage One: Trigger
An update may be triggered when:
- A component's state changes.
- A component receives new props.
- A context value used by a component changes.
- A parent component renders again and renders its children.
- An external store or subscription notifies React through an appropriate integration.
3.2 Stage Two: Render
During rendering, React calls relevant component functions and calculates the next UI description. Rendering should remain free from unintended side effects because React may perform rendering work more than once in development or interrupt and restart certain work in supported rendering scenarios.
3.3 Stage Three: Commit
During the commit stage, React applies the completed update. React Native coordinates changes with its rendering infrastructure so the corresponding native interface can be updated.
3.4 Native Display and Frame Rendering
After the relevant updates are processed, the platform must display the interface within its frame budget. At 60 frames per second, a frame takes approximately 16.67 milliseconds. At 120 frames per second, a frame takes approximately 8.33 milliseconds. Actual performance depends on the device, refresh rate, rendering workload, and platform scheduling.
| Stage |
Purpose |
Example |
| Trigger |
Starts an update |
A button updates state. |
| Render |
Calculates the next UI |
The counter component reads the new state. |
| Commit |
Applies completed changes |
The updated text is committed to the interface. |
| Display |
Shows the result on screen |
The device presents the updated frame. |
4. React Native Architecture and Rendering
React Native's rendering architecture explains how React's JavaScript components are translated into native interface elements. Modern React Native uses a New Architecture built around components such as JSI, Fabric, and TurboModules. Exact implementation details and available capabilities depend on the React Native version and libraries in use.
4.1 JavaScript Runtime
The JavaScript runtime executes application code, including component functions, event handlers, state updates, and many business-logic operations. Hermes is a JavaScript engine commonly used with React Native.
Expensive synchronous JavaScript work can delay event processing and make interactions appear unresponsive. Examples include repeatedly sorting large arrays, parsing large data structures, and performing complex calculations during rendering.
4.2 React Reconciler
The React reconciler determines how the new component tree differs from the previous one. It identifies the work needed to bring the interface into alignment with the latest application state.
4.3 Fabric Renderer
Fabric is React Native's modern rendering system. It supports the coordination of React's rendering work with native views and enables capabilities such as improved interoperability between JavaScript and native rendering operations.
4.4 JavaScript Interface (JSI)
JSI provides a JavaScript interface for interacting with native implementations without relying exclusively on the older asynchronous bridge serialization model. It is a foundational part of the New Architecture.
4.5 TurboModules
TurboModules provide a modern mechanism for native modules, allowing JavaScript to access platform functionality through generated interfaces and the New Architecture infrastructure where supported.
4.6 Understanding the Main Components
| Component |
Primary Responsibility |
| JavaScript runtime |
Executes application JavaScript. |
| React reconciler |
Calculates the required UI updates. |
| Fabric |
Coordinates React Native rendering with native views. |
| JSI |
Provides JavaScript-to-native interoperability infrastructure. |
| TurboModules |
Modern infrastructure for native modules. |
| Native platform |
Displays views and handles platform-specific work. |
5. What Causes a Component to Re-Render?
Re-rendering is not automatically a performance problem. React uses rendering to keep the interface synchronized with application data. The objective is to avoid unnecessary expensive work, not to prevent every render.
5.1 State Changes
When a state setter receives a new value, React may schedule an update for the component. If the next state is identical to the previous state according to React's state comparison behavior, React may skip some rendering work.
import React, { useState } from 'react';
import { Text, Button, View } from 'react-native';
export default function Example() {
const [name, setName] = useState('Alex');
return (
<View>
<Text>Hello, {name}</Text>
<Button
title="Change Name"
onPress={() => setName('Jordan')}
/>
</View>
);
}
5.2 Props Changes
Props are values passed from a parent component to a child. When the parent supplies different props, the child may render again.
function Greeting({ name }) {
return <Text>Welcome, {name}</Text>;
}
function HomeScreen() {
return <Greeting name="Alex" />;
}
5.3 Parent Re-Renders
When a parent renders again, React generally evaluates its child components again unless an applicable optimization or reconciliation behavior allows work to be skipped.
function Child() {
console.log('Child rendered');
return <Text>Child component</Text>;
}
function Parent() {
const [count, setCount] = React.useState(0);
return (
<View>
<Text>Count: {count}</Text>
<Button
title="Increase"
onPress={() => setCount(value => value + 1)}
/>
<Child />
</View>
);
}
When count changes, Parent renders again. Child may also execute again even though it does not receive a changing prop.
5.4 Context Changes
Components that consume a context value may render again when that value changes. A frequently changing context containing unrelated values can cause more components to update than necessary.
5.5 External Store Updates
State libraries and external data stores can notify subscribed components when values change. Performance depends on subscription granularity, selector design, and whether components subscribe only to the data they need.
6. Reconciliation and the Virtual UI Description
React represents the interface through a component tree and reconciles new render results against the previous tree. Developers sometimes use the term Virtual DOM when discussing this process, although React Native ultimately displays native platform views rather than a browser DOM.
6.1 Component Type and Identity
React uses component types, positions, and keys to determine whether existing component state can be preserved or whether a component should be replaced. Changing a component's type or key can cause it to be remounted, resetting its local state.
6.2 Stable Keys in Lists
Keys help React identify list items across insertions, deletions, and reordering. Stable keys are especially important for dynamic lists.
const users = [
{ id: 'u1', name: 'Asha' },
{ id: 'u2', name: 'Rahul' },
{ id: 'u3', name: 'Meera' }
];
function UserList() {
return (
<View>
{users.map(user => (
<Text key={user.id}>{user.name}</Text>
))}
</View>
);
}
Avoid using array indexes as keys when items can be inserted, removed, or reordered. Index keys can associate component state with the wrong item.
7. Understanding React Native Performance
Performance includes several dimensions. An application may start quickly but scroll poorly, or scroll smoothly while responding slowly to button presses. Each problem needs to be measured separately.
| Performance Area |
Description |
Example Problem |
| Startup time |
Time required to become usable |
Slow initial screen. |
| Rendering |
Cost of calculating and applying UI changes |
Expensive component tree. |
| Interaction responsiveness |
How quickly the application responds to input |
Delayed button response. |
| Scrolling |
Consistency of list movement |
Stuttering FlatList. |
| Animation |
Smoothness of visual transitions |
Janky gestures. |
| Memory usage |
RAM required by the application |
Large image cache. |
| Network performance |
Efficiency of remote requests and data handling |
Long loading indicators. |
| Battery consumption |
Energy required for application work |
Unnecessary background activity. |
8. JavaScript Thread and UI Thread
React Native applications involve JavaScript execution and native platform work. The JavaScript thread commonly handles React logic, event handlers, and application code, while the native UI thread handles native view operations and other platform-specific responsibilities. Other threads may also be involved.
8.1 JavaScript Thread
The JavaScript thread can become overloaded when the application performs expensive computations, excessive synchronous processing, or large amounts of work in a single event-loop task.
Possible symptoms include:
- Delayed response to taps.
- Late processing of touch-related JavaScript handlers.
- Slow screen transitions that depend on JavaScript work.
- Delayed list updates.
- Long pauses while processing large datasets.
8.2 UI Thread
The UI thread is responsible for native interface work, including operations related to displaying and interacting with views. Expensive native layout, drawing, or view operations may cause visible frame drops.
8.3 Example of Expensive JavaScript Work
function expensiveCalculation(items) {
let total = 0;
for (let i = 0; i < items.length; i++) {
total += items[i].price * items[i].quantity;
}
return total;
}
This calculation is reasonable for small arrays, but processing very large arrays repeatedly during rendering can be expensive. Profile the real workload before deciding whether to memoize, batch, simplify, or move work elsewhere.
8.4 Ways to Reduce Thread Pressure
- Avoid unnecessary synchronous loops and repeated parsing.
- Keep render functions predictable and efficient.
- Paginate large datasets.
- Use list virtualization for long collections.
- Use appropriate native or background processing solutions for genuinely expensive tasks.
- Use animation libraries that can execute supported animations on the UI thread.
- Avoid logging excessively in performance-critical paths.
9. Measuring Performance Before Optimizing
Performance optimization should begin with measurement. A change that appears beneficial in code may not improve the actual user experience. Establish a baseline, identify the bottleneck, make one targeted change, and measure again.
9.1 Development and Release Builds
Development builds include additional checks and debugging overhead. Their performance may differ significantly from optimized release builds. Use development builds to investigate behavior, then verify performance in a release or appropriately configured profiling build.
9.2 React Native DevTools
React Native DevTools provides debugging capabilities for supported React Native versions, including JavaScript debugging, component inspection, and performance-related tools. The exact panels available depend on the version.
9.3 React Profiler
The React Profiler helps identify which components rendered, how often they rendered, and the relative cost of rendering work. Use it to locate expensive components instead of adding memoization everywhere.
9.4 Native Profiling Tools
- Android Studio Profiler and system tracing tools can help investigate CPU, memory, rendering, and frame performance on Android.
- Xcode Instruments can help analyze CPU usage, memory, allocations, and other performance characteristics on iOS.
- React Native performance monitors can help identify frame-rate issues during investigation, but should not be treated as the only measurement source.
9.5 Performance Investigation Workflow
- Reproduce the issue on a real device when possible.
- Record the device, operating system, React Native version, and build type.
- Identify the slow interaction or screen.
- Measure render duration, frame drops, memory use, or startup time as appropriate.
- Use a profiler to locate the dominant source of work.
- Make one focused change.
- Repeat the same test under comparable conditions.
- Confirm that the optimization did not introduce bugs or excessive complexity.
10. Preventing Unnecessary Re-Renders
Unnecessary re-renders can waste CPU time when components repeatedly execute even though their displayed output does not need to change. The correct solution depends on the component tree, state ownership, prop stability, and measured bottlenecks.
10.1 Keep State Close to Where It Is Used
When state is placed too high in the component tree, changing it can cause a large part of the screen to render again. Move state closer to the components that need it when doing so preserves the correct application design.
10.2 Split Large Components
Large components that combine unrelated state and UI responsibilities can be harder to optimize. Splitting them into focused components can improve readability and create clearer opportunities for memoization.
10.3 Avoid State Updates That Do Not Change Data
Do not update state unnecessarily when the underlying value has not changed. Avoid recreating objects or arrays in state without a reason, especially when they trigger expensive downstream work.
10.4 Use Functional State Updates
Functional updates are useful when the next state depends on the previous state.
setCount(previousCount => previousCount + 1);
This approach avoids relying on a potentially outdated state value captured by a callback.
11. React.memo for Component Optimization
React.memo is a higher-order component that can skip rendering a function component when its props have not changed according to its comparison behavior. By default, React compares each prop using shallow reference equality.
11.1 Example Without React.memo
import React from 'react';
import { Text, View } from 'react-native';
function UserCard({ name }) {
console.log('UserCard rendered');
return (
<View>
<Text>{name}</Text>
</View>
);
}
export default UserCard;
11.2 Example With React.memo
import React from 'react';
import { Text, View } from 'react-native';
const UserCard = React.memo(function UserCard({ name }) {
console.log('UserCard rendered');
return (
<View>
<Text>{name}</Text>
</View>
);
});
export default UserCard;
If a parent renders again while name remains unchanged, React can skip rendering UserCard. This is not guaranteed to improve performance in every case, and memoization does not prevent updates caused by the component's own state or consumed context.
11.3 When to Use React.memo
- For expensive child components that receive stable props.
- For repeated list rows with meaningful rendering cost.
- When profiling shows that parent updates cause avoidable work.
11.4 When Not to Use React.memo
- For trivial components where the comparison costs more than rendering.
- When props change on almost every render.
- When memoization makes code difficult to understand without a measured benefit.
12. useMemo for Expensive Calculations
useMemo caches the result of a calculation between renders while its dependencies remain unchanged. It can help when a calculation is expensive or when stable object identity is needed for a measured optimization.
12.1 Example
import React, { useMemo, useState } from 'react';
import { Text, View, Button } from 'react-native';
export default function ProductSummary({ products }) {
const [refreshCount, setRefreshCount] = useState(0);
const totalPrice = useMemo(() => {
return products.reduce((total, product) => {
return total + product.price * product.quantity;
}, 0);
}, [products]);
return (
<View>
<Text>Total: {totalPrice}</Text>
<Button
title="Refresh Counter"
onPress={() => setRefreshCount(value => value + 1)}
/>
<Text>Refreshes: {refreshCount}</Text>
</View>
);
}
The calculation can be skipped when the products reference remains unchanged. If the parent creates a new products array on every render, the calculation may still run again.
12.2 Important Dependency Rule
Every reactive value used by the calculation should be represented in the dependency list. Omitting dependencies can lead to stale results. Also, do not use useMemo as a substitute for correct state management or as a guarantee that a value will be preserved forever.
13. useCallback for Stable Functions
useCallback caches a function reference until its dependencies change. It is especially useful when passing callbacks to memoized children or when a stable function reference is required by another hook.
13.1 Example
import React, { useCallback, useState } from 'react';
import { Text, View, Button } from 'react-native';
const ActionButton = React.memo(function ActionButton({ onPress }) {
console.log('ActionButton rendered');
return (
<Button title="Increase" onPress={onPress} />
);
});
export default function CounterScreen() {
const [count, setCount] = useState(0);
const [message, setMessage] = useState('Ready');
const increaseCount = useCallback(() => {
setCount(previous => previous + 1);
}, []);
return (
<View>
<Text>Count: {count}</Text>
<Text>Status: {message}</Text>
<ActionButton onPress={increaseCount} />
<Button
title="Change Status"
onPress={() => setMessage('Updated')}
/>
</View>
);
}
The functional state update allows increaseCount to use an empty dependency list because it does not capture the current count value. A different callback that reads changing props or state must include the relevant dependencies.
13.2 Common Mistake
Using useCallback on every function without measuring the effect can add complexity without improving performance. It does not prevent a component from rendering by itself; it only stabilizes a function reference when dependencies remain unchanged.
14. Comparison of React Performance Hooks
| Feature |
Purpose |
Typical Use |
| React.memo |
Can skip a component render when props are unchanged |
Expensive child components. |
| useMemo |
Can reuse a calculated value |
Expensive filtering, sorting, or derived data. |
| useCallback |
Can reuse a function reference |
Callbacks passed to memoized children. |
| useState |
Stores local component state |
Interactive UI values. |
| useRef |
Stores a mutable value without causing a render when changed |
References and values that do not directly drive UI. |
15. Optimizing FlatList Performance
FlatList is a virtualized list component designed to render a manageable subset of a long dataset rather than mounting every item simultaneously. Correct configuration can improve scrolling responsiveness and reduce memory use.
15.1 Basic FlatList Example
import React from 'react';
import { FlatList, Text, View } from 'react-native';
const products = [
{ id: '1', name: 'Laptop' },
{ id: '2', name: 'Keyboard' },
{ id: '3', name: 'Mouse' }
];
export default function ProductList() {
return (
<FlatList
data={products}
keyExtractor={item => item.id}
renderItem={({ item }) => (
<View>
<Text>{item.name}</Text>
</View>
)}
/>
);
}
15.2 Use Stable Keys
Each item should have a stable, unique key. Use a database identifier or another persistent ID whenever possible.
15.3 Memoize Expensive List Rows
import React from 'react';
import { Text, View } from 'react-native';
const ProductRow = React.memo(function ProductRow({ item }) {
return (
<View>
<Text>{item.name}</Text>
<Text>Price: {item.price}</Text>
</View>
);
});
export default ProductRow;
Memoization is most useful when row props remain stable. Creating a new object for every row on every update may prevent React.memo from skipping work.
15.4 Use getItemLayout for Fixed-Size Rows
When every row has a known, fixed height, getItemLayout can allow FlatList to calculate item positions without measuring each row dynamically.
<FlatList
data={products}
keyExtractor={item => item.id}
getItemLayout={(data, index) => ({
length: 64,
offset: 64 * index,
index
})}
renderItem={({ item }) => (
<ProductRow item={item} />
)}
/>
The example assumes that each row is exactly 64 layout units high. If rows contain variable-height content, this fixed-size calculation is not valid unless the layout accounts for the actual dimensions.
15.5 Common FlatList Configuration Options
| Prop |
Purpose |
Guidance |
| initialNumToRender |
Controls initial items rendered |
Choose enough items to fill the initial viewport without excessive work. |
| maxToRenderPerBatch |
Controls items rendered in each batch |
Balance fill rate and responsiveness. |
| windowSize |
Controls the number of viewport lengths maintained around the visible area |
Smaller windows can reduce memory but may expose blank areas during fast scrolling. |
| updateCellsBatchingPeriod |
Controls the interval between cell-render batches |
Tune only after measuring scrolling behavior. |
| getItemLayout |
Provides item layout information |
Use when dimensions can be calculated reliably. |
| removeClippedSubviews |
Can detach off-screen native views from their backing superview on supported platforms |
Test carefully because it can cause visual issues in some layouts. |
| keyExtractor |
Returns an item's key |
Use stable, unique identifiers. |
| extraData |
Provides an additional marker for rendering changes outside the data prop |
Use when the list depends on external state that must trigger an update. |
These props are not universal fixes. Changing windowSize or batching values can improve one workload and worsen another. Test with realistic datasets and device capabilities.
16. ScrollView Versus FlatList
ScrollView generally renders its children together, while FlatList virtualizes a long collection. Selecting the right component is an important performance decision.
| Feature |
ScrollView |
FlatList |
| Rendering approach |
Typically renders all children |
Virtualizes list items |
| Small collections |
Often suitable |
Suitable but may be unnecessary |
| Large collections |
May use substantial memory |
Generally more suitable |
| Item recycling or virtualization |
Not designed for long-list virtualization |
Maintains a render window |
| Specialized list controls |
Requires more manual implementation |
Includes item rendering and list-specific props |
Use ScrollView for small, bounded content such as a settings page or short form. Use FlatList for long or dynamically growing collections. For very large or complex lists, specialized libraries may offer additional capabilities, but their compatibility and trade-offs should be checked.
17. Optimizing State Management
State management affects how frequently components update and how much of the interface responds to each change. A well-designed state structure makes data ownership clear and minimizes unrelated updates.
17.1 Keep Local State Local
Use local state for values that belong to one screen or component, such as whether a dropdown is open. Avoid placing every small UI value into a global store when only one component needs it.
17.2 Split Frequently Changing Context
If a context combines unrelated values that change at different rates, split the context or use a subscription approach that selects only the required data. This can reduce unnecessary updates in consumers.
17.3 Avoid Duplicating Derived State
If a value can be calculated from existing state and props, consider calculating it rather than storing a second copy that must be synchronized.
const [products, setProducts] = useState([]);
const totalItems = products.reduce(
(total, product) => total + product.quantity,
0
);
17.4 Normalize Large Datasets When Appropriate
For complex datasets, storing entities by ID and keeping ordered ID arrays can make updates more targeted. Whether normalization improves performance depends on the data model and state-management library.
18. Image and Asset Optimization
Images can significantly affect memory use, loading time, and scrolling performance. A high-resolution image may occupy a large amount of decoded memory even when displayed at a small size.
18.1 Resize Images Appropriately
Use image assets with dimensions appropriate for their display size. Avoid downloading a full-resolution photograph when a small thumbnail is sufficient.
18.2 Choose Suitable Formats
Choose image formats based on platform support, transparency requirements, visual quality, and file size. Compress images while maintaining acceptable quality.
18.3 Provide Dimensions When Possible
Knowing an image's intended width and height can help reserve layout space before it loads and reduce layout movement.
18.4 Use Caching Thoughtfully
Image-caching libraries can reduce repeated downloads and improve scrolling in image-heavy screens. Check platform compatibility, cache policies, memory behavior, and maintenance status before choosing a library.
18.5 Example
import React from 'react';
import { Image, View } from 'react-native';
export default function ProfilePhoto() {
return (
<View>
<Image
source={{ uri: 'https://example.com/profile-small.jpg' }}
style={{ width: 96, height: 96, borderRadius: 48 }}
resizeMode="cover"
accessibilityLabel="Profile photo"
/>
</View>
);
}
The URL is illustrative. In a real application, use a valid image endpoint and consider loading, error, caching, and offline behavior.
19. Animation Performance
Animations create visual feedback and help users understand transitions. Poorly implemented animations can produce dropped frames, particularly when expensive JavaScript work competes with animation updates.
19.1 Avoid Excessive JavaScript-Driven Animation Work
Animations that require JavaScript to calculate and send every frame may become choppy if the JavaScript thread is overloaded. Where appropriate, use the native driver supported by React Native's Animated API or a suitable animation library that can execute supported animations on the UI thread.
19.2 Animated API Example
import React, { useEffect, useRef } from 'react';
import { Animated, View } from 'react-native';
export default function FadeInBox() {
const opacity = useRef(new Animated.Value(0)).current;
useEffect(() => {
Animated.timing(opacity, {
toValue: 1,
duration: 500,
useNativeDriver: true
}).start();
}, [opacity]);
return (
<View>
<Animated.View
style={{
width: 100,
height: 100,
backgroundColor: 'steelblue',
opacity
}}
/>
</View>
);
}
The native driver supports specific animated properties, such as opacity and transforms. It does not support every layout property. Verify API behavior for the React Native version in use.
19.3 Animation Best Practices
- Prefer transform and opacity animations when suitable.
- Avoid triggering expensive React state updates for every animation frame.
- Keep animated components small and focused.
- Profile gesture-driven screens on lower-end devices.
- Respect accessibility settings, including reduced-motion preferences where applicable.
20. Navigation and Screen Performance
Navigation can affect startup time, memory usage, and the responsiveness of screen transitions. Performance depends on the navigation library, transition implementation, mounted screen count, and the amount of work performed during navigation.
20.1 Avoid Unnecessary Work During Navigation
Do not run expensive calculations or large synchronous transformations inside navigation event handlers or screen render functions when they can be performed efficiently elsewhere.
20.2 Lazy Loading and Screen Lifecycle
Some navigation systems support lazy screen mounting or options that control screen lifecycle behavior. Configure these carefully because delaying mounting may reduce initial work but introduce loading delays when the user first opens a screen.
20.3 Clean Up Effects and Subscriptions
Listeners, timers, and subscriptions should be cleaned up when they are no longer needed. This helps avoid duplicate work and unintended updates.
import React, { useEffect } from 'react';
function ExampleScreen({ subscribe }) {
useEffect(() => {
const unsubscribe = subscribe(() => {
// Handle a relevant event.
});
return () => {
unsubscribe();
};
}, [subscribe]);
return null;
}
This example assumes subscribe returns an unsubscribe function. Adapt cleanup logic to the actual API.
21. API Requests and Data Loading
Network delays are not the same as rendering delays, but inefficient data loading can make the application feel slow. Large responses may also create rendering and memory costs after the data arrives.
21.1 Use Pagination
Load a manageable page of results rather than retrieving an entire large dataset immediately. Fetch additional pages as the user approaches the end of the list.
21.2 Avoid Duplicate Requests
Track request state and use suitable caching or data-fetching strategies to prevent unnecessary duplicate calls. Handle cancellation or stale responses when the screen changes or a newer request supersedes an earlier one.
21.3 Keep Loading States Lightweight
Display an appropriate loading indicator or placeholder without mounting a large amount of unnecessary UI. Ensure that loading, error, empty, and success states are clearly handled.
21.4 Example API Loading Pattern
import React, { useEffect, useState } from 'react';
import { ActivityIndicator, Text, View } from 'react-native';
export default function UserScreen() {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
useEffect(() => {
let active = true;
async function loadUser() {
try {
const response = await fetch(
'https://jsonplaceholder.typicode.com/users/1'
);
if (!response.ok) {
throw new Error('Unable to load user');
}
const data = await response.json();
if (active) {
setUser(data);
}
} catch (err) {
if (active) {
setError('Could not load user information.');
}
} finally {
if (active) {
setLoading(false);
}
}
}
loadUser();
return () => {
active = false;
};
}, []);
if (loading) {
return <ActivityIndicator />;
}
if (error) {
return <Text>{error}</Text>;
}
return (
<View>
<Text>{user?.name}</Text>
<Text>{user?.email}</Text>
</View>
);
}
The active flag prevents this example from updating state after cleanup. For production code, also consider request cancellation where supported, retry behavior, timeouts, caching, and the possibility of multiple concurrent requests.
22. Memory Management and Memory Leaks
Memory issues can cause sluggishness, crashes, or operating-system termination. JavaScript objects, native views, images, subscriptions, and cached data can all contribute to memory usage.
22.1 Common Causes
- Event listeners that remain active after a screen is removed.
- Timers that are never cleared.
- Large arrays retained unnecessarily.
- Unbounded image or data caches.
- Subscriptions that are not released.
- Keeping large objects in closures or global state without a reason.
22.2 Clean Up Timers
import React, { useEffect } from 'react';
export default function TimerExample() {
useEffect(() => {
const timerId = setInterval(() => {
console.log('Periodic task');
}, 5000);
return () => {
clearInterval(timerId);
};
}, []);
return null;
}
22.3 Memory Optimization Practices
- Release resources when they are no longer needed.
- Use list virtualization for large collections.
- Limit the size of in-memory caches.
- Inspect memory behavior during repeated navigation and long sessions.
- Avoid keeping duplicate copies of large datasets.
- Test on devices with limited available memory.
23. Application Startup Performance
Startup performance affects how quickly a user can begin interacting with the application. Heavy initialization, large dependency graphs, synchronous storage work, and unnecessary network requests can increase startup time.
23.1 Reduce Initial Work
- Load only the data required for the first screen.
- Defer nonessential work until after the initial interface is usable.
- Review large dependencies and remove unused packages.
- Avoid performing expensive transformations before the first screen appears.
- Use appropriate release build settings.
- Measure cold-start and warm-start behavior separately.
23.2 Avoid Blocking Initialization
Where possible, move nonessential tasks out of the critical startup path. This does not mean every task should be delayed; authentication, essential configuration, and required application state may need to be available before navigation proceeds.
24. Conditional Rendering and Component Design
Conditional rendering is a normal React pattern. Performance problems can arise when conditions cause large subtrees to mount and unmount repeatedly or when expensive computations are embedded directly in render logic.
24.1 Conditional UI Example
function StatusMessage({ loading, error, user }) {
if (loading) {
return <Text>Loading...</Text>;
}
if (error) {
return <Text>Unable to load data.</Text>;
}
if (!user) {
return <Text>No user found.</Text>;
}
return <Text>Welcome, {user.name}</Text>;
}
24.2 Avoid Defining Components Inside Components Without a Reason
Defining a component function inside another component can create a new component type during each parent render. Depending on how it is used, this can cause remounting and loss of child state. Prefer defining reusable component types at module scope when appropriate.
24.3 Avoid Unnecessary Inline Heavy Calculations
Simple expressions are usually fine in render functions. Expensive sorting, filtering, parsing, or complex transformations may need memoization or a different data-processing approach when profiling confirms they are costly.
25. Common Performance Mistakes
| Mistake |
Why It Can Be a Problem |
Better Approach |
| Rendering thousands of rows inside ScrollView |
Large initial render and memory cost |
Use FlatList or another suitable virtualized list. |
| Using unstable list keys |
Incorrect identity and state reuse |
Use stable, unique IDs. |
| Memoizing every component |
Extra complexity and comparison overhead |
Profile and optimize measured bottlenecks. |
| Missing hook dependencies |
Stale values and incorrect behavior |
Use complete dependency lists. |
| Updating state during render |
Can trigger repeated renders or errors |
Update state in events or appropriate effects. |
| Doing heavy work in event handlers |
Blocks JavaScript processing |
Reduce, batch, or offload expensive work where appropriate. |
| Loading oversized images |
Higher memory use and slower loading |
Resize, compress, and cache appropriately. |
| Ignoring cleanup |
Duplicate work and retained resources |
Clean up timers, listeners, and subscriptions. |
| Testing only in development |
Development overhead distorts results |
Verify with release-like builds. |
| Optimizing without measurements |
May not address the real problem |
Profile before and after changes. |
26. Troubleshooting React Native Rendering Problems
26.1 A Component Renders Too Often
Possible causes: state is placed too high in the component tree, the parent repeatedly renders, props are recreated, context updates are too broad, or subscriptions are too frequent.
Solutions:
- Use React Profiler to identify the components doing the work.
- Move local state closer to where it is needed.
- Stabilize expensive child props where useful.
- Split unrelated context values or refine subscriptions.
- Use React.memo only when its comparison can avoid meaningful work.
26.2 FlatList Scrolls Poorly
Possible causes: complex rows, large images, unstable keys, expensive state updates, excessive list-window configuration, or slow item rendering.
Solutions:
- Profile the list while scrolling.
- Keep row components lightweight.
- Use stable keys and appropriate image dimensions.
- Consider getItemLayout for fixed-height rows.
- Test FlatList settings with realistic data and devices.
- Avoid expensive synchronous calculations during each row render.
26.3 Buttons Respond Slowly
Possible causes: expensive JavaScript tasks, synchronous data processing, too many updates, or slow work in the event handler.
Solutions: simplify the handler, avoid unnecessary calculations, move appropriate work out of the critical interaction path, and measure JavaScript-thread activity.
26.4 Animations Are Choppy
Possible causes: JavaScript-driven frame updates, expensive layout changes, large view hierarchies, or native rendering bottlenecks.
Solutions: use supported native-driven animation techniques, reduce work during gestures, simplify the animated view hierarchy, and profile both JavaScript and native performance.
26.5 Memory Usage Increases Over Time
Possible causes: uncleared subscriptions, retained data, oversized images, or unbounded caches.
Solutions: inspect memory snapshots, test repeated screen navigation, release unused resources, and set sensible cache limits.
27. Practical Project: Optimize a Product Listing Screen
Build a product listing screen that displays product names, images, prices, and availability. Start with a working implementation, measure its behavior, and then apply optimizations only where they improve performance.
27.1 Project Requirements
- Display a list of at least 500 sample products.
- Include a thumbnail, product name, and price for each item.
- Provide a search input for filtering products.
- Display a loading indicator during data loading.
- Handle empty results and request errors.
- Support smooth scrolling on a real Android or iOS device.
- Track component rendering during development.
- Compare performance before and after optimization.
27.2 Sample Product Row
import React from 'react';
import { Image, Text, View } from 'react-native';
const ProductRow = React.memo(function ProductRow({ product }) {
return (
<View style={{ flexDirection: 'row', padding: 12 }}>
<Image
source={{ uri: product.thumbnail }}
style={{ width: 64, height: 64 }}
resizeMode="cover"
/>
<View style={{ flex: 1, marginLeft: 12 }}>
<Text>{product.name}</Text>
<Text>${product.price.toFixed(2)}</Text>
</View>
</View>
);
});
export default ProductRow;
This example illustrates a memoized row. A complete implementation should also handle invalid image URLs, missing fields, accessibility, loading states, and appropriate styling.
27.3 Optimization Plan
- Implement the first version using FlatList.
- Use stable product IDs for keys.
- Record scrolling behavior and initial load time.
- Profile expensive rows and search calculations.
- Memoize rows or derived search results if measurements justify it.
- Resize and cache thumbnails when appropriate.
- Adjust list window and batching settings if needed.
- Repeat tests with a release build and realistic data.
- Document the improvements and any trade-offs.
28. Best Practices for React Native Performance
- Measure performance before making changes.
- Test on real devices and optimized builds.
- Keep component rendering pure and predictable.
- Place state near the components that need it.
- Use React.memo, useMemo, and useCallback selectively.
- Maintain stable list keys.
- Use FlatList for large collections.
- Keep list rows lightweight and avoid unnecessary nested views.
- Optimize image size and loading behavior.
- Keep expensive synchronous JavaScript work out of critical interaction paths.
- Use appropriate animation techniques for smooth motion.
- Clean up timers, listeners, and subscriptions.
- Handle network loading, errors, pagination, and cancellation appropriately.
- Monitor memory usage during long sessions.
- Review dependencies and update libraries carefully.
- Retest performance after significant architectural or dependency changes.
29. Practice Exercises
- Create a counter screen and identify which components render when the count changes.
- Create a parent component with two children and observe the effect of parent state updates.
- Apply React.memo to one child and compare its rendering behavior.
- Use useMemo to cache an expensive filter operation on a sample dataset.
- Use useCallback to stabilize a handler passed to a memoized child.
- Build a FlatList containing 1,000 items and observe scrolling performance.
- Compare ScrollView and FlatList with the same dataset.
- Optimize a list containing images of different sizes.
- Create an animation and compare supported native-driven and JavaScript-driven behavior.
- Identify and fix a timer or event-listener cleanup issue.
- Profile a slow screen and record the largest rendering or processing costs.
- Prepare a short report showing baseline measurements and results after optimization.
30. Interview Questions and Answers
Q1. What is rendering in React Native?
Answer: Rendering is the process through which React evaluates components and calculates the user interface based on the current props, state, and context. React Native coordinates the resulting updates with native views.
Q2. What is the difference between rendering and committing?
Answer: Rendering calculates the next UI representation. Committing applies the completed changes. The two stages perform different responsibilities.
Q3. What can trigger a re-render?
Answer: State changes, new props, context updates, parent renders, and updates from subscribed external stores can trigger rendering work.
Q4. Does every re-render update native views?
Answer: No. A component can execute again while producing the same UI. React reconciles the result and applies the necessary updates rather than recreating the entire native interface for every render.
Q5. What is React.memo?
Answer: React.memo can skip rendering a function component when its props remain unchanged according to its comparison behavior. It does not block updates caused by the component's own state or consumed context.
Q6. What is the difference between useMemo and useCallback?
Answer: useMemo caches a calculated value, while useCallback caches a function reference. Both use dependency lists and should be applied when they provide a meaningful benefit.
Q7. Why are stable keys important in FlatList?
Answer: Stable keys let React identify items across updates and reordering. They help preserve correct component identity and avoid incorrect state reuse.
Q8. When should FlatList be used instead of ScrollView?
Answer: FlatList is generally more suitable for long or dynamically growing collections because it virtualizes items. ScrollView is often appropriate for short, bounded content.
Q9. What is the JavaScript thread responsible for?
Answer: It executes JavaScript application code, React logic, event handlers, and many state-processing tasks. Heavy synchronous work can delay interaction handling.
Q10. What is Fabric?
Answer: Fabric is React Native's modern rendering system that coordinates React's rendering work with native views.
Q11. How can you investigate unnecessary renders?
Answer: Use React Profiler and component inspection to identify which components render, how frequently they render, and where rendering time is spent.
Q12. Why should performance be tested in release builds?
Answer: Development builds include debugging and validation overhead that can distort timing. Release-build testing provides a more representative view of production behavior.
Q13. How can image loading affect performance?
Answer: Large images can increase download time, decoded memory usage, and list-rendering costs. Appropriate dimensions, compression, caching, and loading behavior can help.
Q14. How can memory leaks be prevented?
Answer: Clean up timers and listeners, unsubscribe from external sources, avoid retaining unnecessary data, and monitor memory usage during long-running sessions.
Q15. What is the first step in performance optimization?
Answer: Reproduce the issue and measure it. Profiling helps identify the actual bottleneck before changes are made.
31. Quick Revision Checklist
- Understand the trigger, render, and commit stages.
- Know the roles of the JavaScript runtime, reconciler, Fabric, JSI, and TurboModules.
- Recognize common causes of re-renders.
- Understand component identity, reconciliation, and stable keys.
- Use React.memo for suitable child components.
- Use useMemo for expensive calculations when justified.
- Use useCallback to stabilize function references when useful.
- Use FlatList for large collections and configure it based on measurements.
- Understand JavaScript-thread and UI-thread bottlenecks.
- Measure startup, rendering, scrolling, animation, and memory separately.
- Test release builds on real devices.
- Optimize images, API loading, and navigation.
- Clean up timers, subscriptions, and event listeners.
- Compare performance before and after each significant change.
32. Official Documentation and Learning Resources
33. Conclusion
Understanding React Native rendering and performance helps developers build applications that are responsive, efficient, and reliable. Rendering is a normal part of React's update process, but unnecessary work can become expensive when components, calculations, lists, images, or animations grow in complexity.
The most effective approach is to understand the rendering lifecycle, structure state carefully, maintain stable component and list identities, and apply memoization only when it addresses a measured problem. Developers should also consider JavaScript execution, native rendering, memory usage, network loading, and startup behavior.
Use profiling tools to establish a baseline, optimize the bottleneck, and test the result on realistic devices and datasets. These practices provide a solid foundation for creating high-quality React Native applications.
Continue Learning: Visit React Native Training at JustAcademy or register for a course demo to explore additional React Native topics and practical application development.